/*
 * Copyright 2012 Igor Benicio de Mesquita
 * benizzio@gmail.com
 * 
 * This file is part of fleaNJECT.
 * 
 * fleaNJECT is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as 
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 * 
 * fleaNJECT is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with fleaNJECT.  If not, see <http://www.gnu.org/licenses/>.
 */

package fleanject.di.impl;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.inject.Inject;
import javax.inject.Named;

import fleanject.di.AbstractConfiguration;
import fleanject.di.ConfigurationValidator;
import fleanject.di.annotation.Component;
import fleanject.di.exception.DIConfigurationException;
import fleanject.di.exception.DIException;


public class AnnotationConfigurationValidator implements ConfigurationValidator {
	
	@Override
	public void validateConfiguration(AbstractConfiguration configuration, Map<String, Object> nonComponentDependencyMap) {
		
		AnnotationConfiguration annotationConfiguration = (AnnotationConfiguration) configuration;
		
		Map<String, Class<?>> componentClassMap = new HashMap<String, Class<?>>();
		
		for(Class<?> configurationClass : annotationConfiguration.getConfigurations()) {
			
			this.verifyConfigurationClass(configurationClass, nonComponentDependencyMap, componentClassMap);
			
			String componentKey = configurationClass.getAnnotation(Named.class) != null ? 
					configurationClass.getAnnotation(Named.class).value() : configurationClass.getName();
			
			componentClassMap.put(componentKey, configurationClass);
		}
		
		this.verifyNoQualifierDependencyResolution(componentClassMap);
		
		this.verifyCircularDependencies(componentClassMap);
	}
	
	private void verifyConfigurationClass(Class<?> configurationClass, Map<String, Object> nonComponentDependencyMap, 
			Map<String, Class<?>> componentClassMap) {
		
		// Verifies the existence of the nullary constructor
		try {
			configurationClass.getConstructor((Class<?>[]) null);
		}
		catch(SecurityException e) {
			throw new DIException("Error accessing " +  configurationClass.getName() + " constructor for verifications", e);
		}
		catch(NoSuchMethodException e) {
			throw new DIConfigurationException("Class must have a public nullary constructor (to keep it simple)", 
					configurationClass.getName(), e);
		}
		
		// Verifies the class @Component annotation
		Component componentAnnotation = configurationClass.getAnnotation(Component.class);
		
		if(componentAnnotation == null) {
			throw new DIConfigurationException("Class is not a @Component", configurationClass.getName());
		}
		
		// Verifies the class @Named annotation, if existent
		Named namedAnnotation = configurationClass.getAnnotation(Named.class);
		
		if(namedAnnotation != null) {
			
			if(namedAnnotation.value().isEmpty()) {
				throw new DIConfigurationException("@Named value must be specified", configurationClass.getName());
			}
			
			// Verifies if the @Named qualifier conflicts with a non-component dependency
			if(nonComponentDependencyMap != null && nonComponentDependencyMap.containsKey(namedAnnotation.value())) {
				throw new DIConfigurationException("@Named value (" + namedAnnotation.value() + ") conflicts with an already " +
						"configured non-component dependency", configurationClass.getName());
			}
		
			// Verifies if the @Named qualifier is duplicated
			if(componentClassMap.containsKey(namedAnnotation.value())) {
				throw new DIConfigurationException("@Named value (" + namedAnnotation.value() + ") is duplicated", 
						configurationClass.getName());
			}
		}
		
		//verifies methods annotations
		for(Method method : configurationClass.getMethods()) {
			
			if(method.getAnnotation(Inject.class) != null) {
				
				Named namedMethodAnnotation = method.getAnnotation(Named.class);
				
				if(namedMethodAnnotation != null && namedMethodAnnotation.value().isEmpty()) {
					throw new DIConfigurationException("@Named value must be specified", configurationClass.getName(), 
							method.getName());
				}
				else if(method.getParameterTypes().length != 1) {
					throw new DIConfigurationException("Methods with @Inject must have only one parameter to inject " +
							"(to keep it simple).", configurationClass.getName(), method.getName());
				}
			}
		}
	}
	
	private void verifyNoQualifierDependencyResolution(Map<String, Class<?>> componentClassMap) {
		
		for(Class<?> componentClass : componentClassMap.values()) {
			for(Method method : componentClass.getMethods()) {
				
				
				if(method.getAnnotation(Inject.class) != null && method.getAnnotation(Named.class) == null && 
						!componentClassMap.containsKey(method.getParameterTypes()[0].getName())) {
					
					int assignableClassCount = 0;
					for(Class<?> verificationComponentClass : componentClassMap.values()) {
						if(method.getParameterTypes()[0].isAssignableFrom(verificationComponentClass)) { 
							assignableClassCount++; 
						}
					}
					
					String errorMessage = "Impossible to resolve dependency for method - ";
					if(assignableClassCount == 0) {
						throw new DIConfigurationException(errorMessage + "no assignable component found", 
								componentClass.getName(), method.getName());
					}
					else if(assignableClassCount > 1) {
						throw new DIConfigurationException(errorMessage + "more than one assignable component found " +
								"(try using a @Named qualifier)", componentClass.getName(), method.getName());
					}
				}
			}
		}
	}
	
	private void verifyCircularDependencies(Map<String, Class<?>> componentClasses) {
		
		List<String> injectionBranchLog = new LinkedList<String>();
		
		// start verification recursively into injection tree
		for(Class<?> componentClass : componentClasses.values()) {
			this.navigateInjectionInBranch(componentClass, injectionBranchLog, componentClasses);
		}
	}
	
	private void navigateInjectionInBranch(Class<?> componentClass, List<String> injectionBranchLog, 
			Map<String, Class<?>> componentClassMap) {
		
		Named namedAnnotation = componentClass.getAnnotation(Named.class);
		
		String componentKey = namedAnnotation != null ? namedAnnotation.value() : componentClass.getName();
		// verifies if this component already exists in branch, meaning a circular dependency
		// in that case, an exception breaks the execution
		this.verifyCircularDependencyInBranch(componentKey, injectionBranchLog);
		
		// if not, adds this component class to the branch
		injectionBranchLog.add(componentKey);
		
		for(Method method : componentClass.getMethods()) {
			
			// looks for other dependencies to continue branch
			if(method.getAnnotation(Inject.class) != null) {
				
				// if it's a @Component dependency, continues branch navigation recursively
				String dependecyKey = this.resolveDependencyKey(method, componentClassMap);
				Class<?> componentDependencyClass = componentClassMap.get(dependecyKey);
				if(componentDependencyClass != null) {
					this.navigateInjectionInBranch(componentDependencyClass, injectionBranchLog, componentClassMap);
				}
			}
		}
		
		injectionBranchLog.remove(componentKey);
	}
	
	private void verifyCircularDependencyInBranch(String key, List<String> injectionBranchLog) {
		
		if(injectionBranchLog.contains(key)) {
			
			StringBuffer message = new StringBuffer("Circular dependency found! Description: "); 
			String connection = "-> ";
			for(String componentName : injectionBranchLog) {
				message.append(connection + componentName);
			}
			message.append(connection + key);
			throw new DIConfigurationException(message.toString());
		}
	}
	
	private String resolveDependencyKey(Method injectionMethod, Map<String, Class<?>> componentClassMap) {		
		// Look for a @Named qualifier
		Named namedMethodAnnotation = injectionMethod.getAnnotation(Named.class);
		if(namedMethodAnnotation != null) {
			return namedMethodAnnotation.value();
		}
		else {
			
			// Look if the parameter class for the injection method is mapped to get it's name
			Class<?> injectionParameterClass = injectionMethod.getParameterTypes()[0];
			if(componentClassMap.containsKey(injectionParameterClass.getName())) {
				return injectionParameterClass.getName();
			}
			// If not mapped, looks for a assignable bean class to get it's name
			else {
				for(Entry<String, Class<?>> componentClassMapEntry : componentClassMap.entrySet()) {
					if(injectionParameterClass.isAssignableFrom(componentClassMapEntry.getValue())) {
						return componentClassMapEntry.getKey();
					}
				}
			}
		}
		
		//due to previous validation this line is unreachable
		return null;
	}
}